import { Scene } from '@babylonjs/core/scene';
import { AbstractComposite, IAbstractCompositeOptions } from './abstract-composite';
import { CreateUIRectangle, UIRectangle } from '../control/ui-rectangle';
import { ANCHOR } from '../control/abstract-base';
import { CreateUIButton, UIButton } from '../control/ui-button';
import { AnimationFrame, ITimer } from '../../animation/animation-frame';
import { Matrix, Vector3 } from '@babylonjs/core/Maths/math.vector';
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
import { Plane } from '@babylonjs/core/Maths/math.plane';
import { Helper } from '../../tool/helper';
import { PointerEventTypes, PointerInfo } from '@babylonjs/core/Events/pointerEvents';
import { AbstractControl, IAbstractControlOptions } from '../control/abstract-control';
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';


/**
 * 滚动条容器组件参数对象
 * @param unitWidth 成员组件宽度
 * @param unitHeight 成员组件高度
 * @param rowLength 每行成员组件数量
 * @param columnLength 每列成员组件数量（刚好装满容器时的）
 * @param spacing 成员组件间隔
 * @param margin 边距
 * @param blockSize 拖动块宽度
 * @param backgroundColor 背景颜色
 * @param blockColor 拖动块颜色
 * @param backgroundRenderingGroupId 背景渲染组id
 * @param backgroundAlpha 背景透明度
 * @param pointerThrottle 指针事件节流间隔
 * @param cornerRadius 背景圆角
 * @param alwaysInoperable 成员总是不可操作
 */
interface IAbstractScrollListOptions extends IAbstractCompositeOptions {
    unitWidth?: number,
    unitHeight?: number,
    rowLength?: number,
    columnLength?: number,
    spacing?: number,
    margin?: number,
    blockSize?: number,
    backgroundColor?: string,
    blockColor?: string,
    backgroundRenderingGroupId?: number,
    blockRenderingGroupId?: number,
    backgroundAlpha?: number,
    pointerThrottle?: number,
    cornerRadius?: number,
    alwaysInoperable?: boolean
}

/**
 * 滚动条容器组件
 * @param name 名称
 * @param scene 所在场景
 * @param options 参数对象
 */
abstract class AbstractScrollList<T1 extends AbstractControl, T2 extends IAbstractControlOptions> extends AbstractComposite {

    private _background!: UIRectangle;
    private _block!: UIButton;
    private _board!: UIRectangle;
    private _req!: ITimer;
    private _blockStart = new Vector3();
    private _blockEnd = new Vector3();
    private _blockRange: number[] = [];
    private _location = { x: 0, y: 0 };
    private _height!: number;
    private _width!: number;
    private _memberHeight: number = 0;
    private _contentHeight!: number;
    private _dragHeight!: number;
    private _clipPlaneTop!: Plane;
    private _clipPlaneBottom!: Plane;
    private _tm0 = new Matrix();
    private _blockSize!: number;
    private _dragPlane!: Plane;
    private _start = 0;
    private _pointerHandle!: { (e: PointerInfo): void };
    private _bottomTarget!: TransformNode;
    private _topTarget!: TransformNode;
    private _updateReq!: ITimer;

    /**
     * 成员组件
     */
    protected members: Array<T1> = [];

    /**
     * 成员组件容器
     */
    protected memberContainer!: TransformNode;

    constructor(name: string, scene: Scene, options?: IAbstractScrollListOptions) {
        super(name, scene, options);
        this.updateBlockSize();
    }

    public dispose(): void {
        super.dispose();
        AnimationFrame.cancel(this._req);
        AnimationFrame.cancel(this._updateReq);
        this.scene.onPointerObservable.removeCallback(this._pointerHandle);
        this._background.dispose();
        this._block.dispose();
        this._board.dispose();
        this.clear();
    }

    protected initializeOptions(options: IAbstractScrollListOptions): IAbstractScrollListOptions {
        const ops = super.initializeOptions(options) as IAbstractScrollListOptions;
        ops.unitWidth = ops.unitWidth || 0.12;
        ops.unitHeight = ops.unitHeight || 0.1;
        ops.rowLength = ops.rowLength || 3;
        ops.columnLength = ops.columnLength || 4;
        ops.spacing = ops.spacing || 0.01;
        ops.margin = ops.margin || 0.02;
        ops.blockSize = ops.blockSize || 0.03;
        ops.backgroundColor = ops.backgroundColor || '#000000';
        ops.blockColor = ops.blockColor || '#cecece';
        ops.backgroundRenderingGroupId = ops.backgroundRenderingGroupId || 1;
        ops.blockRenderingGroupId = ops.blockRenderingGroupId || 2;
        ops.backgroundAlpha = ops.backgroundAlpha || 0.7;
        ops.pointerThrottle = ops.pointerThrottle || 50;
        ops.cornerRadius = ops.cornerRadius || 5;
        ops.alwaysInoperable = !!ops.alwaysInoperable;
        return ops;
    }

    /**
     * 添加成员组件
     * @param name 名称
     * @param options 参数对象
     * @returns 被添加的组件
     */
    public add(name: string, options?: T2): T1 | null {
        for (const mb of this.members) {
            if (mb.name === name) return null;
        }
        const option = this.options as IAbstractScrollListOptions;

        const ops: T2 = options || {} as T2;
        ops.doNotUseProportion = true;
        ops.canvasSize = option.canvasSize!;
        ops.width = option.unitWidth;
        ops.height = option.unitHeight;
        ops.anchor = ANCHOR.LEFT_TOP;
        this.setLocation(this.members.length);
        ops.left = this._location.x;
        ops.top = this._location.y;
        ops.renderingGroupId = option.blockRenderingGroupId;

        const mb = this.generateMember(name, ops)!;

        Helper.getChildMeshes(mb.container).forEach(m => {
            m.material!.clipPlane = this._clipPlaneBottom;
            m.material!.clipPlane2 = this._clipPlaneTop;
        });
        mb.container.parent = this.memberContainer;
        this.members.push(mb);
        this._memberHeight = Math.round(this.members.length / option.rowLength! + 0.49999) * (option.unitHeight! + option.spacing!) + option.spacing!;
        this.updateBlockSize();
        this.setMemberOperable(this.members.length - 1);
        return mb;
    }

    /**
     * 创建成员组件
     * @param options 参数对象
     */
    protected abstract generateMember(name: string, options?: T2): T1;

    /**
     * 移除成员组件
     * @param name 组件名称
     */
    public remove(name: string): void {
        const option = this.options as IAbstractScrollListOptions;
        for (let i = 0; i < this.members.length; i++) {
            const mb = this.members[i];
            if (mb.name !== name) continue;
            mb.container.parent = null;
            mb.dispose();
            this.members.splice(i, 1);
            break;
        }
        this._memberHeight = Math.round(this.members.length / option.rowLength!) * (option.unitHeight! + option.spacing!) + option.spacing!;
        this.updateBlockSize();
    }

    /**
     * 清除成员组件
     */
    public clear(): void {
        this.members.forEach(sg => {
            sg.dispose();
            sg.container.parent = null;
        });
        this.members.length = 0;
    }

    protected initializeComponents(): void {
        this.initializeElement();
        this.initializeDragSlide();
        this.initializeClipping();
    }

    private setMemberOperable(index: number): void {
        const ops = this.options as IAbstractScrollListOptions;
        const mb = this.members[index];
        const dy = this.memberContainer.position.y;
        const ht = mb.top + dy;
        const hb = ht - ops.unitHeight!;
        const outOfRange = (ht > this._height - ops.margin!) || (hb < 0 + ops.margin!);
        mb.inoperable = ops.alwaysInoperable || outOfRange;
        mb.isEnabled = !outOfRange;
    }

    private initializeClipping(): void {
        const ops = this.options as IAbstractScrollListOptions;
        this._clipPlaneBottom = new Plane(0, -1, 0, 0);
        this._clipPlaneTop = new Plane(0, 1, 0, 0);

        this._bottomTarget = new TransformNode(this.name + '-bottom-target', this.scene);
        this._bottomTarget.parent = this.container;
        this._bottomTarget.position.set(0, -this._height * 0.5 + ops.margin!, 0);

        this._topTarget = new TransformNode(this.name + '-top-target', this.scene);
        this._topTarget.parent = this.container;
        this._topTarget.position.set(0, this._height * 0.5 - ops.margin!, 0);

        const rot = new Matrix();
        this._updateReq = { observable: this.scene.onBeforeCameraRenderObservable };
        AnimationFrame.request(() => {
            this.container.getWorldMatrix().getRotationMatrixToRef(rot);

            this._clipPlaneBottom.normal.set(0, -1, 0);
            Vector3.TransformCoordinatesToRef(this._clipPlaneBottom.normal, rot, this._clipPlaneBottom.normal);
            Plane.FromPositionAndNormalToRef(this._bottomTarget.absolutePosition, this._clipPlaneBottom.normal, this._clipPlaneBottom);

            this._clipPlaneTop.normal.set(0, 1, 0);
            Vector3.TransformCoordinatesToRef(this._clipPlaneTop.normal, rot, this._clipPlaneTop.normal);
            Plane.FromPositionAndNormalToRef(this._topTarget.absolutePosition, this._clipPlaneTop.normal, this._clipPlaneTop);
        }, 0, 200, this._updateReq);
    }

    protected updateBlockSize(): void {
        const ops = this.options as IAbstractScrollListOptions;
        const ratio = this._contentHeight / (this._memberHeight < this._contentHeight ? this._contentHeight : this._memberHeight);
        this._blockSize = this._contentHeight * ratio;
        this._block.height = this._blockSize;
        this._dragHeight = this._contentHeight - this._blockSize;
        this._blockRange[0] = this._height - ops.margin! - this._dragHeight;
        this._blockRange[1] = this._height - ops.margin!;
        const pct = this.memberContainer.position.y <= 0 ? 0 : this.memberContainer.position.y / (this._memberHeight - this._contentHeight);
        this._block.top = this._height - ops.margin! - pct * this._dragHeight;
        (this._block.container as AbstractMesh).isVisible = this._dragHeight !== 0;
    }

    protected setLocation(index: number): void {
        const ops = this.options as IAbstractScrollListOptions;
        const r = Math.floor(index / ops.rowLength!);
        const c = index % ops.rowLength!;
        this._location.y = this._height - (r * (ops.unitHeight! + ops.spacing!) + ops.spacing! + ops.margin!);
        this._location.x = c * (ops.unitWidth! + ops.spacing!) + ops.spacing! + ops.margin!;
    }

    private initializeDragSlide(): void {
        const ops = this.options as IAbstractScrollListOptions;
        this._req = {};
        this._pointerHandle = (e: PointerInfo) => {
            switch (e.type) {
                case PointerEventTypes.POINTERDOWN:
                    const pi = Helper.pickInfo(e, this.scene);
                    if (!pi) return;
                    const pm = pi.pickedMesh;
                    if (!pm) return;

                    const mat = this._tm0.copyFrom(this.elementContainer.getWorldMatrix()).invert();
                    this._dragPlane = Plane.FromPositionAndNormal(pi.pickedPoint!, this.elementContainer.forward);
                    this._blockStart.copyFrom(pi.pickedPoint!);
                    Vector3.TransformCoordinatesToRef(this._blockStart, mat, this._blockStart);

                    if (pm.id === this._block.container.id) {
                        this._start = 1;
                    } else if (pm.id === this._board.container.id) {
                        this._start = 2;
                        this._block.top = this._blockStart.y + this._blockSize * 0.5 - ops.margin!;
                        this.updateBlock();
                    }
                    break;
                case PointerEventTypes.POINTERMOVE:
                    if (!this._start) return;
                    if (!e.pickInfo) return;
                    AnimationFrame.throttleByTime(this._req, ops.pointerThrottle!, () => {
                        const ray = e.pickInfo!.ray;
                        if (!ray) return;
                        const dis = ray.intersectsPlane(this._dragPlane);
                        if (!dis) return;
                        const s = this.container.absoluteScaling.y;
                        this._blockEnd.copyFrom(ray.direction).scaleInPlace(dis).addInPlace(ray.origin);
                        const mat = this._tm0.copyFrom(this.elementContainer.getWorldMatrix()).invert();
                        Vector3.TransformCoordinatesToRef(this._blockEnd, mat, this._blockEnd);
                        if (this._start === 1) {
                            this._block.top += (this._blockEnd.y - this._blockStart.y) * s / this.elementContainer.absoluteScaling.y;
                            this.updateBlock();
                            this._blockStart.copyFrom(this._blockEnd);
                        } else if (this._start === 2) {
                            this._block.top = this._blockEnd.y + this._blockSize * 0.5 - ops.margin!;
                            this.updateBlock();
                        }
                    }, false);

                    break;
                case PointerEventTypes.POINTERUP:
                    this._start = 0;
                    break;
            }
        }
        this.scene.onPointerObservable.add(this._pointerHandle);
    }

    protected updateBlock(): void {
        const ops = this.options as IAbstractScrollListOptions;
        this._block.top = Math.min(Math.max(this._block.top, this._blockRange[0]), this._blockRange[1]);
        const dragLength = this._contentHeight - this._block.top + ops.spacing! * 2;
        const percentage = Math.min(Math.max(dragLength / this._dragHeight, 0), 1);
        const y = (this._memberHeight - this._contentHeight) * percentage;
        if (this._blockRange[0] < this._blockRange[1]) this.memberContainer.position.y = y;
        for (let i = 0; i < this.members.length; i++) {
            this.setMemberOperable(i);
        }
    }

    private initializeElement(): void {
        const ops = this.options as IAbstractScrollListOptions;

        this.memberContainer = new TransformNode(this.name + '-member-container', this.scene);
        this.memberContainer.parent = this.elementContainer;

        this._contentHeight = (ops.unitHeight! + ops.spacing!) * ops.columnLength! + ops.spacing!;
        this._width = ops.unitWidth! * ops.rowLength! + ops.spacing! * (ops.rowLength! + 1) + ops.margin! * 2 + ops.blockSize!;
        this._height = (ops.unitHeight! + ops.spacing!) * ops.columnLength! + ops.spacing! + ops.margin! * 2;
        const bg = CreateUIRectangle(this.name + '-background', this.scene, {
            anchor: ANCHOR.LEFT_BOTTOM,
            width: this._width,
            height: this._height,
            background: ops.backgroundColor,
            cornerRadius: ops.cornerRadius,
            renderingGroupId: ops.backgroundRenderingGroupId,
            doNotUseProportion: true,
            inoperable: true,
            canvasSize: ops.canvasSize!,
            alpha: ops.backgroundAlpha,
        });
        bg.container.parent = this.elementContainer;
        this._background = bg;
        this.boundControls.push(bg);

        const bk = CreateUIButton(this.name + '-block', this.scene, {
            anchor: ANCHOR.RIGHT_TOP,
            width: ops.blockSize!,
            height: this._height,
            left: this._width - ops.margin!,
            top: this._height - ops.margin!,
            background: ops.blockColor!,
            renderingGroupId: ops.blockRenderingGroupId!,
            cornerRadius: 2,
            doNotUseProportion: true,
            text: ' ',
            canvasSize: ops.canvasSize!,
        });
        bk.container.parent = this.elementContainer;
        this._block = bk;

        const bd = CreateUIRectangle(this.name + '-board', this.scene, {
            anchor: ANCHOR.RIGHT_TOP,
            width: ops.spacing! + ops.blockSize! + ops.margin!,
            height: this._height,
            left: this._width,
            top: this._height,
            background: '#000000',
            doNotUseProportion: true,
            renderingGroupId: ops.backgroundRenderingGroupId,
            depth: 0.01,
            alpha: 0,
            cornerRadius: 5,
            canvasSize: ops.canvasSize!
        });
        bd.container.parent = this.elementContainer;
        this._board = bd;
    }

}


export { AbstractScrollList };
export type { IAbstractScrollListOptions };